package org.snakebattle.utils.reflection;

import java.lang.reflect.GenericDeclaration;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.Stack;

/**
* Alex Tracer (c) 2009
*/
public class ReflectionUtils {

  /**
   * Для некоторого класса определяет каким классом был параметризован один из его предков с generic-параметрами.
   *
   * @param actualClass   анализируемый класс
   * @param genericClass  класс, для которого определяется значение параметра
   * @param parameterIndex номер параметра
   * @return        класс, являющийся параметром с индексом parameterIndex в genericClass
   */
  public static Class<?> getGenericParameterClass(final Class<?> actualClass, final Class<?> genericClass, final int parameterIndex) {
    // Прекращаем работу если genericClass не является предком actualClass.
    if (!genericClass.isAssignableFrom(actualClass.getSuperclass())) {
      throw new IllegalArgumentException("Class " + genericClass.getName() + " is not a superclass of "
          + actualClass.getName() + ".");
    }

    // Нам нужно найти класс, для которого непосредственным родителем будет genericClass.
    // Мы будем подниматься вверх по иерархии, пока не найдем интересующий нас класс.
    // В процессе поднятия мы будем сохранять в genericClasses все классы - они нам понадобятся при спуске вниз.

    // Проейденные классы - используются для спуска вниз.
    Stack<ParameterizedType> genericClasses = new Stack<ParameterizedType>();

    // clazz - текущий рассматриваемый класс
    Class<?> clazz = actualClass;

    while (true) {
      Type genericSuperclass = clazz.getGenericSuperclass();
      boolean isParameterizedType = genericSuperclass instanceof ParameterizedType;
      if (isParameterizedType) {
        // Если предок - параметризованный класс, то запоминаем его - возможно он пригодится при спуске вниз.
        genericClasses.push((ParameterizedType) genericSuperclass);
      } else {
        // В иерархии встретился непараметризованный класс. Все ранее сохраненные параметризованные классы будут бесполезны.
        genericClasses.clear();
      }
      // Проверяем, дошли мы до нужного предка или нет.
      Type rawType = isParameterizedType ? ((ParameterizedType) genericSuperclass).getRawType() : genericSuperclass;
      if (!rawType.equals(genericClass)) {
        // genericClass не является непосредственным родителем для текущего класса.
        // Поднимаемся по иерархии дальше.
        clazz = clazz.getSuperclass();
      } else {
        // Мы поднялись до нужного класса. Останавливаемся.
        break;
      }
    }

    // Нужный класс найден. Теперь мы можем узнать, какими типами он параметризован.
    Type result = genericClasses.pop().getActualTypeArguments()[parameterIndex];

    while (result instanceof TypeVariable && !genericClasses.empty()) {
      // Похоже наш параметр задан где-то ниже по иерархии, спускаемся вниз.

      // Получаем индекс параметра в том классе, в котором он задан.
      int actualArgumentIndex = getParameterTypeDeclarationIndex((TypeVariable<?>) result);
      // Берем соответствующий класс, содержащий метаинформацию о нашем параметре.
      ParameterizedType type = genericClasses.pop();
      // Получаем информацию о значении параметра.
      result = type.getActualTypeArguments()[actualArgumentIndex];
    }

    if (result instanceof TypeVariable) {
      // Мы спустились до самого низа, но даже там нужный параметр не имеет явного задания.
      // Следовательно из-за "Type erasure" узнать класс для параметра невозможно.
      throw new IllegalStateException("Unable to resolve type variable " + result + "."
          + " Try to replace instances of parametrized class with its non-parameterized subtype.");
    }

    if (result instanceof ParameterizedType) {
      // Сам параметр оказался параметризованным.
      // Отбросим информацию о его параметрах, она нам не нужна.
      result = ((ParameterizedType) result).getRawType();
    }

    if (result == null) {
      // Should never happen. :)
      throw new IllegalStateException("Unable to determine actual parameter type for "
          + actualClass.getName() + ".");
    }

    if (!(result instanceof Class)) {
      // Похоже, что параметр - массив или что-то еще, что не является классом.
      throw new IllegalStateException("Actual parameter type for " + actualClass.getName() + " is not a Class.");
    }

    return (Class<?>) result;
  }

  public static int getParameterTypeDeclarationIndex(final TypeVariable<?> typeVariable) {
    GenericDeclaration genericDeclaration = typeVariable.getGenericDeclaration();

    // Ищем наш параметр среди всех параметров того класса, где определен нужный нам параметр.
    @SuppressWarnings("rawtypes")
	TypeVariable[] typeVariables = genericDeclaration.getTypeParameters();
    Integer actualArgumentIndex = null;
    for (int i = 0; i < typeVariables.length; i++) {
      if (typeVariables[i].equals(typeVariable)) {
        actualArgumentIndex = i;
        break;
      }
    }
    if (actualArgumentIndex != null) {
      return actualArgumentIndex;
    } else {
      throw new IllegalStateException("Argument " + typeVariable.toString() + " is not found in "
          + genericDeclaration.toString() + ".");
    }
  }
}